Skip to content

feat: Public Tasks API with API key auth and webhook callbacks#1

Open
pec1985 wants to merge 9 commits intomainfrom
feat/public-tasks-api
Open

feat: Public Tasks API with API key auth and webhook callbacks#1
pec1985 wants to merge 9 commits intomainfrom
feat/public-tasks-api

Conversation

@pec1985
Copy link

@pec1985 pec1985 commented Feb 11, 2026

Summary

Adds a public REST API (/api/v1/tasks) that enables external services to programmatically create coding sessions, interact with them, and receive webhook notifications when work completes. Includes a settings UI for users to create and manage their API keys.

This is the foundation for integrations like CI bots, Slack bots, or any external service that needs to trigger "go to this repo, fix something, and open a PR" workflows.

Architecture

sequenceDiagram
    participant External as External Service
    participant API as /api/v1/tasks
    participant Auth as API Key Auth
    participant DB as PostgreSQL
    participant Sandbox as Agentuity Sandbox
    participant OC as OpenCode (AI)
    participant GH as GitHub
    participant WH as Webhook URL

    External->>API: POST /api/v1/tasks
    Note right of External: {repoUrl, prompt, webhookUrl}
    API->>Auth: Validate Bearer token
    Auth-->>API: User context
    API->>DB: INSERT session (status: creating)
    API-->>External: 201 {taskId, status: creating}

    Note over API,Sandbox: Background (async)
    API->>Sandbox: Create sandbox
    Sandbox->>GH: Clone repo
    API->>OC: Create session + send prompt
    API->>DB: UPDATE status → active

    Note over OC,GH: AI works...
    OC->>OC: Analyze, edit files, commit
    OC->>GH: Push + open PR
    OC-->>API: session.idle event (via SSE)

    API->>DB: UPDATE status → completed
    API->>WH: POST {taskId, status: completed, prUrl}
    WH-->>External: 200 OK

    Note over External,API: Optional: continue conversation
    External->>API: POST /tasks/:id/messages
    Note right of External: {text: "Also add tests"}
    API->>OC: Forward prompt
    OC->>OC: Continue working...
    OC-->>API: session.idle
    API->>WH: POST {taskId, status: completed}

    External->>API: DELETE /tasks/:id
    API->>Sandbox: Destroy
    API->>DB: DELETE session
Loading

What's New

1. Public Tasks API — 5 endpoints

All authenticated via API key (Authorization: Bearer <key>), using Better Auth's existing apikey table and the createApiKeyMiddleware that was already exported but never wired up.

Method Endpoint Purpose
POST /api/v1/tasks Create a coding task (accepts repoUrl, branch, prompt, webhookUrl)
GET /api/v1/tasks/:id Get task status, PR URL, metadata
POST /api/v1/tasks/:id/messages Send follow-up messages to continue the conversation
GET /api/v1/tasks/:id/events SSE stream for real-time AI activity
DELETE /api/v1/tasks/:id Destroy sandbox and clean up

2. Webhook Callbacks

When a task completes (OpenCode emits session.idle), the system POSTs to the caller's webhookUrl:

{
  "taskId": "uuid",
  "status": "completed",
  "repoUrl": "https://github.com/org/repo",
  "prUrl": "https://github.com/org/repo/pull/42",
  "completedAt": "2026-02-11T..."
}
  • 3 retry attempts with exponential backoff (1s → 2s → 4s)
  • 10s timeout per attempt
  • Non-retryable on 4xx (except 429)

3. Session Completion Detection

The SSE proxy in chat.ts now detects session.idle events and transitions task status from activecompleted. This only applies to API-created tasks (where metadata.source === 'api'). Web UI sessions are completely untouched.

4. API Key Management UI

New settings section where users can create, view, and delete API keys:

  • Create keys via dialog with custom name
  • One-time key display after creation with copy-to-clipboard
  • Key list with name, creation date, expiry, and last-used timestamps
  • Toggle key prefix visibility for identification
  • Delete with confirmation
  • Clean empty state with call-to-action
  • Helper text showing how to use keys with the Tasks API

Files Changed

File Change
src/routes/tasks.ts New — Public Tasks API (5 endpoints)
src/lib/webhook.ts New — Webhook delivery utility with retry logic
src/web/components/settings/ApiKeySettings.tsx New — API key management UI component
src/api/index.ts Mount /api/v1/tasks routes with apiKeyMiddleware before the session auth catch-all
src/routes/chat.ts Added completion detection for API-created tasks + webhook trigger
src/web/components/pages/SettingsPage.tsx Added API Keys card section to settings

Design Decisions

  • No schema changes — Webhook URL and source: 'api' flag stored in existing metadata jsonb column
  • Auto-managed workspace — API tasks grouped under an auto-created "API Tasks" workspace per user
  • Ownership checks — Tasks scoped to the API key's user; callers can only access their own tasks
  • Zero impact on existing app — Completion detection gated on metadata.source === 'api'
  • Direct REST calls for UI — API key UI uses fetch() to Better Auth endpoints (/api/auth/api-key/*)

Example Usage

# Create a task
curl -X POST https://coder.example.com/api/v1/tasks \
  -H "Authorization: Bearer ag_..." \
  -H "Content-Type: application/json" \
  -d '{
    "repoUrl": "https://github.com/org/repo",
    "prompt": "Fix the auth bug in src/login.ts and open a PR",
    "webhookUrl": "https://my-service.com/callback"
  }'

# Returns: { "taskId": "uuid", "status": "creating" }
# Webhook fires when done with PR URL
# Use taskId to send follow-ups or check status

Future Work (not in scope)

  • Rate limiting on API endpoints
  • OpenAPI spec / auto-generated docs
  • Webhook HMAC signature verification
  • Sandbox TTL extension for long-idle API sessions

Summary by CodeRabbit

  • New Features
    • Public Tasks API (/v1/tasks) for programmatic coding tasks: create, fetch, message, stream, delete — gated by API key auth.
    • API Keys settings UI: generate, view, copy, and revoke keys from Settings.
    • Webhook delivery with retry/backoff for task events; fire-and-forget webhook firing on session completion.
    • Real-time event streaming via server-sent events (SSE).

@coderabbitai
Copy link

coderabbitai bot commented Feb 11, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds an API-key–protected public Tasks API at /v1/tasks, a webhook delivery utility with exponential backoff, session-completion detection that triggers asynchronous webhook calls for API-originated sessions, and a new API Keys management UI in Settings.

Changes

Cohort / File(s) Summary
API entry & routing
src/api/index.ts
Imports and mounts apiKeyMiddleware and registers the new /v1/tasks router so the Tasks API is exposed under /v1/tasks using API-key auth while other routes keep existing auth.
Public Tasks API router
src/routes/tasks.ts
Adds a default-exported router implementing POST/GET/DELETE for tasks, POST for follow-up messages, and SSE events streaming. Implements workspace creation, sandbox provisioning, OpenCode session initialization with retries, ownership/validation checks, background task lifecycle updates, and webhook-config handling.
Webhook delivery utility
src/lib/webhook.ts
Adds exported WebhookPayload interface and deliverWebhook(url, payload, options?) that POSTs JSON with per-attempt timeout, exponential backoff, retry rules (abort on non-retryable 4xx except 429), and logging.
Chat session completion integration
src/routes/chat.ts
Adds handleSessionCompletionEvent(event, sessionId, opencodeSessionId) to detect session.idle for API-originated sessions, update session status to completed, and fire-and-forget call to deliverWebhook. Injects calls into SSE/message streaming flows; delivery errors logged non-blockingly.
Web UI — API Keys settings
src/web/components/settings/ApiKeySettings.tsx, src/web/components/pages/SettingsPage.tsx
Adds ApiKeySettings component and renders an “API Keys” card on SettingsPage. Implements listing, creating (one-time revealed key), copy-to-clipboard, deletion with confirmation, per-key metadata display, and related UI states.
🚥 Pre-merge checks | ✅ 1
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@src/routes/chat.ts`:
- Around line 102-119: The code can process duplicate session.idle events and
trigger multiple updates/webhooks; first, read the current row (chatSessions)
via the existing select and check current.status !== 'completed' (or !== 'idle'
depending on your lifecycle) before proceeding, then perform the UPDATE with an
additional WHERE that includes the expected previous status (e.g.,
.where(eq(chatSessions.id, sessionId)).and(eq(chatSessions.status, 'idle'))) so
only the first concurrent updater wins; rely on the returned "updated" value
from the update to decide whether to trigger the webhook (only trigger when
updated is truthy) and avoid multiple webhook calls; use parseMetadata,
sessionId, chatSessions, and the existing update/returning flow to implement
this guard.

In `@src/routes/tasks.ts`:
- Around line 45-64: The getOrCreateApiWorkspace function can race when two
requests create the same workspace; modify the insert path to use an upsert that
ignores conflicts (e.g., use db.insert(...).values(...).onConflictDoNothing())
and then re-query the workspaces table to fetch the existing row if the insert
did nothing; keep the initial select fallback but after the insert use a second
select (or returning when supported) to ensure you return the
created-or-existing workspace and avoid unique constraint errors on
workspaces.name + workspaces.organizationId.
- Around line 333-415: Export the existing handleSessionCompletionEvent from
chat.ts and import it into tasks.ts (or alternatively copy its
completion-detection logic into tasks.ts), then invoke it inside the SSE event
loop in the tasks endpoint right after parsing each JSON event (the block that
currently computes eventSessionId and checks against session.opencodeSessionId);
specifically, call handleSessionCompletionEvent(event, session) (or the
equivalent logic) when the parsed event indicates session completion so
configured webhooks fire; ensure you export the symbol from chat.ts and add the
import to the file that defines the '/:id/events' SSE handler which uses
getOwnedTask and session.opencodeSessionId.
🧹 Nitpick comments (3)
src/routes/chat.ts (1)

348-351: Inconsistent error handling compared to line 137.

The error handler here silently swallows errors with .catch(() => {}), while the equivalent call in line 137 logs the error. Consider consistent logging:

♻️ Proposed fix for consistent logging
-						handleSessionCompletionEvent(event, session.id, session.opencodeSessionId!).catch(
-							() => {},
-						);
+						handleSessionCompletionEvent(event, session.id, session.opencodeSessionId!).catch(
+							(err) => console.error(`[webhook] Completion event handling failed:`, err),
+						);
src/routes/tasks.ts (2)

168-176: Consider guarding against null session.

While unlikely, if both the insert (due to conflict) and the subsequent select fail (e.g., deleted between operations), session would be undefined, causing line 261 to fail. The current code assumes this won't happen, which is reasonable but could be made defensive:

🛡️ Proposed defensive check
 	let session = insertedRows[0];
 	if (!session) {
 		const [existing] = await db
 			.select()
 			.from(chatSessions)
 			.where(eq(chatSessions.id, sessionId))
 			.limit(1);
 		session = existing;
 	}
+	if (!session) {
+		return c.json({ error: 'Failed to create task' }, 500);
+	}

427-438: Consider handling destroySandbox failure gracefully.

If destroySandbox throws (e.g., sandbox already gone, network error), the task record remains in the database. Depending on desired behavior, you may want to proceed with deletion even if sandbox cleanup fails:

♻️ Proposed fix for resilient cleanup
 	if (session.sandboxId) {
 		const sandboxCtx: SandboxContext = {
 			sandbox: c.var.sandbox,
 			logger: c.var.logger,
 		};
-		await destroySandbox(sandboxCtx, session.sandboxId);
+		try {
+			await destroySandbox(sandboxCtx, session.sandboxId);
+		} catch (err) {
+			c.var.logger.warn('Failed to destroy sandbox, proceeding with task deletion', {
+				sandboxId: session.sandboxId,
+				error: err,
+			});
+		}
 		removeOpencodeClient(session.sandboxId);
 	}
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5039478 and 95a7e4c.

📒 Files selected for processing (4)
  • src/api/index.ts
  • src/lib/webhook.ts
  • src/routes/chat.ts
  • src/routes/tasks.ts
🧰 Additional context used
🧬 Code graph analysis (3)
src/routes/chat.ts (4)
src/db/index.ts (1)
  • db (11-11)
src/db/schema.ts (1)
  • chatSessions (25-40)
src/lib/parse-metadata.ts (1)
  • parseMetadata (11-31)
src/lib/webhook.ts (2)
  • WebhookPayload (8-17)
  • deliverWebhook (32-80)
src/api/index.ts (1)
src/auth.ts (1)
  • apiKeyMiddleware (43-43)
src/routes/tasks.ts (4)
src/db/index.ts (1)
  • db (11-11)
src/db/schema.ts (5)
  • workspaces (15-23)
  • chatSessions (25-40)
  • skills (42-53)
  • sources (55-64)
  • userSettings (66-72)
src/lib/encryption.ts (1)
  • decrypt (18-31)
src/lib/parse-metadata.ts (1)
  • parseMetadata (11-31)
🔇 Additional comments (7)
src/lib/webhook.ts (1)

1-80: LGTM! Well-structured webhook delivery with proper retry logic.

The implementation correctly handles:

  • Exponential backoff (1s → 2s → 4s)
  • Non-retryable 4xx responses (except 429)
  • 10s timeout per attempt
  • Clear logging for debugging
src/routes/chat.ts (1)

14-16: LGTM!

Clean imports for the new webhook functionality.

src/api/index.ts (1)

37-40: LGTM!

Correct middleware registration order ensures API key authentication is applied before the catch-all authMiddleware. Both /v1/tasks and /v1/tasks/* patterns are needed to cover the root endpoint and nested routes.

src/routes/tasks.ts (4)

183-257: LGTM!

The background provisioning pattern is well-implemented:

  • Context captured before async to avoid stale references
  • Retry logic for OpenCode session creation (5 attempts, 2s delay)
  • Error handling updates status to 'error' for visibility
  • Initial prompt sent after session is established

272-293: LGTM!

Proper ownership validation and safe metadata extraction.


298-328: LGTM!

Clean implementation matching the pattern in chat.ts, with appropriate simplification (no attachments) for the public API.


1-31: LGTM!

Well-documented module header with clear endpoint descriptions. Imports are properly organized.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/web/components/settings/ApiKeySettings.tsx`:
- Around line 209-220: The empty-state card in ApiKeySettings currently renders
whenever keys.length === 0 even if an error exists; change the conditional so
the "No API keys yet" block only renders when keys.length === 0 AND error is
falsy (i.e., only show the empty state when there is no load error). Locate the
render that checks keys.length === 0 in ApiKeySettings (related symbols: keys,
error, fetchKeys, setError, setKeys, setShowCreate) and update it to
short-circuit on error so the error banner ("Failed to load API keys") and the
empty state are not shown at the same time.
- Around line 110-117: The code assumes data.key always exists: after const key
= data?.key; add a guard so if key is falsy you do not clear the form or close
the dialog; instead set a user-facing error state (create/use something like
setCreateError or showError) and return so the dialog stays open and the user
sees that creation returned no key; if key exists continue to call
setNewlyCreatedKey(key), clear setNewKeyName(''), setShowCreate(false) and await
fetchKeys(); also optionally log/res.json for debugging.
🧹 Nitpick comments (1)
src/web/components/settings/ApiKeySettings.tsx (1)

149-157: Empty catch block doesn't implement the advertised fallback.

The comment says "Fallback: select text" but no fallback is actually implemented. If clipboard access fails (e.g., due to permissions or non-HTTPS context), the user gets no feedback.

🔧 Proposed fix to provide fallback or feedback
 const handleCopy = async (text: string) => {
   try {
     await navigator.clipboard.writeText(text);
     setCopied(true);
     setTimeout(() => setCopied(false), 2000);
   } catch {
-    // Fallback: select text
+    // Clipboard API failed - show brief error or rely on select-all styling
+    // The code element has select-all class so users can manually copy
+    console.warn('Clipboard access denied, user can manually select and copy');
   }
 };
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 95a7e4c and a3f8a74.

📒 Files selected for processing (2)
  • src/web/components/pages/SettingsPage.tsx
  • src/web/components/settings/ApiKeySettings.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
src/web/components/pages/SettingsPage.tsx (1)
src/web/components/settings/ApiKeySettings.tsx (1)
  • ApiKeySettings (49-391)
🔇 Additional comments (6)
src/web/components/settings/ApiKeySettings.tsx (4)

1-47: LGTM!

The imports, interfaces, and helper functions are well-structured. The formatRelative function correctly handles edge cases and provides appropriate fallback to formatDate for older entries.


49-91: LGTM!

State management is well-organized with clear separation of concerns. The useCallback for fetchKeys and proper dependency array in useEffect follow React best practices.


244-323: LGTM!

The key list implementation with two-step delete confirmation provides good UX. The conditional rendering for confirm/cancel states and disabled states during async operations is handled correctly.


333-389: LGTM!

The create dialog has proper accessibility with labeled input and DialogDescription. The Enter-to-submit handler and disabled state logic are correctly implemented.

src/web/components/pages/SettingsPage.tsx (2)

6-6: LGTM!

Import path is correct relative to the file location.


331-338: LGTM!

The API Keys section follows the same Card pattern as the GitHub section above, maintaining UI consistency. The descriptive text clearly explains the purpose of the feature.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/routes/tasks.ts`:
- Around line 221-248: The session creation loop (getOpencodeClient /
client.session.create) can finish without opencodeSessionId and leave the chat
session status as "creating"; change the post-loop handling so that if
opencodeSessionId is still null you set status to "error" and persist the
failure reason: capture the last thrown error (from the catch) or a final
message and include it in the DB update (add and set an error column like
opencodeError or sessionError alongside sandboxId, sandboxUrl,
opencodeSessionId, status, updatedAt) so the final
db.update(chatSessions).set(...) writes status: 'error' and the error metadata
to avoid clients polling forever.
🧹 Nitpick comments (1)
src/web/components/settings/ApiKeySettings.tsx (1)

151-161: Consider cleaning up the timeout on unmount.

The setTimeout at line 155 isn't cleaned up if the component unmounts within 2 seconds of copying, which could trigger a React state update warning on an unmounted component.

♻️ Proposed fix using useRef for timeout cleanup
+import { useCallback, useEffect, useState, useRef } from 'react';

In the component:

+	const copyTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
+
+	useEffect(() => {
+		return () => {
+			if (copyTimeoutRef.current) clearTimeout(copyTimeoutRef.current);
+		};
+	}, []);

 	const handleCopy = async (text: string) => {
 		try {
 			await navigator.clipboard.writeText(text);
 			setCopied(true);
-			setTimeout(() => setCopied(false), 2000);
+			copyTimeoutRef.current = setTimeout(() => setCopied(false), 2000);
 		} catch {
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a3f8a74 and 46c0c80.

📒 Files selected for processing (3)
  • src/routes/chat.ts
  • src/routes/tasks.ts
  • src/web/components/settings/ApiKeySettings.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
src/web/components/settings/ApiKeySettings.tsx (3)
src/web/components/ui/button.tsx (1)
  • Button (52-52)
src/web/components/ui/badge.tsx (1)
  • Badge (26-26)
src/web/components/ui/dialog.tsx (6)
  • Dialog (95-95)
  • DialogContent (100-100)
  • DialogHeader (101-101)
  • DialogTitle (103-103)
  • DialogDescription (104-104)
  • DialogFooter (102-102)
🔇 Additional comments (18)
src/web/components/settings/ApiKeySettings.tsx (8)

1-26: LGTM!

Clean imports and well-defined interface. The ApiKey interface covers all necessary fields for the key management UI.


28-47: LGTM!

Well-implemented date formatting helpers with appropriate null handling and human-friendly relative time display.


49-91: LGTM!

State management is well-organized. The fetchKeys implementation correctly handles errors and uses defensive coding with Array.isArray(data) check.


93-125: LGTM!

The previous review comment about handling missing key in response has been addressed. The error handling at lines 114-116 now properly notifies the user when the key was created but couldn't be retrieved. The subsequent form clear and fetchKeys() call is appropriate since the key exists server-side.


127-149: LGTM!

Solid delete implementation with proper error handling and per-key loading state. Keeping the confirmation buttons visible on failure allows users to retry.


163-210: LGTM!

Well-designed new key banner with clear messaging about the one-time visibility. The select-all styling on the code element provides a good fallback for manual copying.


212-329: LGTM!

The previous review comment about empty state showing alongside error message has been addressed. The conditional at line 213 now correctly checks keys.length === 0 && !error to prevent both UI states from appearing simultaneously. The key list UI with per-item confirmation flow and togglable prefix visibility is well-implemented.


331-394: LGTM!

Well-structured error display, accessible create dialog with proper label association (htmlFor/id), Enter key support, and clear guidance text for API usage.

src/routes/chat.ts (3)

10-16: Imports align with completion + webhook logic.
No issues with the added dependencies for metadata parsing and webhook delivery.


82-140: Completion handling is well-guarded and non-blocking.
The status guard plus conditional update prevents duplicate webhooks, and the async delivery won’t stall SSE.


335-357: SSE loop integration looks solid.
Filtering, completion detection, and malformed-event handling are all in good shape.

src/routes/tasks.ts (7)

1-32: Public Tasks API scaffold and imports look good.
No concerns with the module setup or dependencies.


39-74: Conflict-safe workspace creation is solid.
The upsert + re-fetch path cleanly handles concurrent inserts.


76-89: Ownership check helper is clear and safe.
Returning 404 on mismatched ownership keeps the surface tight.


285-305: Task detail response shape is clean.
The metadata extraction is consistent and client-friendly.


311-340: Message forwarding flow looks correct.
Validation + prompt dispatch are straightforward and safe.


346-433: Verify completion/webhook handling isn’t dependent on SSE consumers.
Completion detection is currently executed inside the SSE loop. If no client connects to /events, session completion might never be processed and webhooks won’t fire. Please confirm there’s another background listener or move completion handling off the request-bound SSE path.


438-463: Cleanup flow is robust.
Sandbox destruction with logging + client removal + DB delete looks good.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/routes/tasks.ts`:
- Around line 372-380: The fetch to the sandbox event stream
(fetch(`${session.sandboxUrl}/event`)) has no timeout and can hang; update the
request to use an AbortSignal.timeout (or create an AbortController with a
setTimeout) so the connection attempt fails after a short initial timeout, then
handle the abort error by writing the same SSE error via stream.writeSSE and
calling stream.close(); ensure you reference the fetch call and the
eventResponse handling (and keep existing stream.writeSSE / stream.close
behavior) so long-lived SSE reads remain unaffected.

In `@src/web/components/settings/ApiKeySettings.tsx`:
- Around line 78-95: In fetchKeys (the useCallback function) clear any previous
error on successful retrieval by calling setError(null) when the response is ok
and after parsing data (before setKeys or immediately after), and also clear
error in the fallback branch where you set keys to an empty array; ensure
setLoading(false) remains in finally so loading state is handled as before. This
change touches fetchKeys, setError, setKeys, and setLoading.
🧹 Nitpick comments (1)
src/routes/tasks.ts (1)

333-340: Consider validating the model format.

If a user passes model: "anthropic" (without the /), the split produces providerID = "anthropic" and modelID = undefined, silently ignoring the model override. This may confuse API consumers who expect an error when providing an invalid format.

💡 Suggested validation
 	try {
-		const [providerID, modelID] = body.model ? body.model.split('/') : [];
+		let providerID: string | undefined;
+		let modelID: string | undefined;
+		if (body.model) {
+			if (!body.model.includes('/')) {
+				return c.json({ error: 'model must be in format "provider/model"' }, 400);
+			}
+			[providerID, modelID] = body.model.split('/');
+		}
 		await client.session.promptAsync({
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 46c0c80 and 48ec784.

📒 Files selected for processing (2)
  • src/routes/tasks.ts
  • src/web/components/settings/ApiKeySettings.tsx
🧰 Additional context used
🧬 Code graph analysis (2)
src/routes/tasks.ts (5)
src/db/index.ts (1)
  • db (11-11)
src/db/schema.ts (5)
  • workspaces (15-23)
  • chatSessions (25-40)
  • skills (42-53)
  • sources (55-64)
  • userSettings (66-72)
src/lib/encryption.ts (1)
  • decrypt (18-31)
src/lib/parse-metadata.ts (1)
  • parseMetadata (11-31)
src/routes/chat.ts (1)
  • handleSessionCompletionEvent (86-140)
src/web/components/settings/ApiKeySettings.tsx (3)
src/web/components/ui/button.tsx (1)
  • Button (52-52)
src/web/components/ui/badge.tsx (1)
  • Badge (26-26)
src/web/components/ui/dialog.tsx (6)
  • Dialog (95-95)
  • DialogContent (100-100)
  • DialogHeader (101-101)
  • DialogTitle (103-103)
  • DialogDescription (104-104)
  • DialogFooter (102-102)
🔇 Additional comments (21)
src/web/components/settings/ApiKeySettings.tsx (13)

1-16: LGTM!

Imports are well-organized and the API_BASE constant provides good centralization for API endpoint management.


17-47: LGTM!

The ApiKey interface properly models the API response, and the date formatting utilities handle null values gracefully. The relative time formatting provides good UX for recent activity.


49-76: LGTM!

State management is well-organized by concern, and the timeout cleanup in useEffect properly prevents memory leaks and state updates on unmounted components.


101-133: LGTM!

The create handler properly validates input, handles both success and error responses, and addresses the edge case where the API returns success without the key value (as noted in past review).


135-157: LGTM!

The delete handler implements a good two-step confirmation pattern with proper loading and error states.


159-169: LGTM!

Good graceful degradation when clipboard API is unavailable, with proper timeout management via the ref.


171-180: LGTM!

Clean dismiss handler and appropriate loading state display.


185-218: LGTM!

Excellent UX for the newly created key banner with clear one-time visibility warning, copy functionality with visual feedback, and proper text wrapping for long keys.


221-232: LGTM!

The empty state properly hides when there's an error (addressing past review feedback), providing clear onboarding guidance for new users.


256-336: LGTM!

Well-structured key list with inline delete confirmation, proper accessibility (button always in DOM), and good use of Tailwind group hover patterns for the delete action reveal.


339-343: LGTM!

Clear error banner styling that's appropriately positioned below the key list.


346-389: LGTM!

Well-structured dialog with proper accessibility (label/input association), keyboard support (Enter to create), and appropriate disabled states during creation.


391-401: LGTM!

Clear contextual help text explaining API key usage with proper formatting for technical details.

src/routes/tasks.ts (8)

15-34: LGTM!

Imports are well-organized and appropriate for the functionality. Good use of randomUUID from node:crypto for generating session IDs.


46-74: LGTM!

The race condition handling with onConflictDoNothing followed by a re-fetch is a solid pattern for concurrent workspace creation.


76-89: LGTM!

Good security practice returning the same "Task not found" error for both non-existent and unauthorized tasks, preventing task ID enumeration attacks.


94-189: LGTM!

The task creation flow is well-structured:

  • Input validation catches missing prompt and invalid webhook URLs
  • Session creation handles race conditions with onConflictDoNothing and re-fetch
  • Conflict handling pattern is consistent with the workspace creation helper

196-274: LGTM!

The background async processing is well-implemented:

  • Captures context variables before the async block to avoid closure issues
  • Retry logic with 5 attempts and 2-second delays for session creation
  • Properly transitions to error status with descriptive metadata on failure
  • Fire-and-forget pattern is appropriate here since the response already instructs clients to poll or use SSE

289-310: LGTM!

Clean implementation with proper ownership verification and safe metadata parsing.


413-416: LGTM!

Good integration of handleSessionCompletionEvent for webhook delivery on session completion. The fire-and-forget pattern with error logging is appropriate here to avoid blocking the SSE stream.


442-467: LGTM!

Good resilience pattern: logs the sandbox destruction failure but proceeds with task deletion to avoid leaving orphaned records. The removeOpencodeClient cleanup ensures no stale client references remain.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/routes/tasks.ts`:
- Around line 335-340: The model parsing currently uses body.model.split('/')
and destructures into providerID and modelID, which drops any additional path
segments (e.g., "openai/gpt-4/turbo"); update the parsing in the tasks route so
providerID is the substring before the first '/' and modelID is the entire
remainder after the first '/', preserving variants. Concretely, in the block
that reads body.model (referencing body.model, providerID, modelID), replace the
split/destructure with logic that finds the first '/' (e.g., indexOf or split
with limit) and assigns providerID = body.model.slice(0, idx) and modelID =
body.model.slice(idx+1), keeping the existing validation that a slash must
exist.
- Around line 112-122: The current webhook URL check only validates scheme but
allows localhost/private addresses; update the validation around the
body.webhookUrl/new URL(...) block to reject internal addresses by: 1)
immediately reject obvious hostnames like "localhost" and IP literals like
"127.0.0.1" or "::1"; 2) resolve the hostname (use dns.promises.lookup or
equivalent in your environment) when the host is not an IP literal, get all
A/AAAA addresses, and check each IP against private/loopback/link-local ranges
(RFC1918: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, ::1,
fc00::/7, fe80::/10, etc.); and 3) if any resolved IP is
private/loopback/link-local, return c.json({ error: 'webhookUrl resolves to a
private or loopback address' }, 400). Ensure DNS resolution errors are handled
and treated as rejection or cause a clear 400 response.
🧹 Nitpick comments (1)
src/routes/tasks.ts (1)

438-442: Avoid exposing internal error details to API clients.

String(error) may include stack traces or internal paths, leaking implementation details. Use a generic error message for the SSE response.

🛡️ Suggested fix
 		} catch (error) {
+			console.error('[tasks/events] SSE stream error:', error);
 			await stream.writeSSE({
-				data: JSON.stringify({ type: 'error', message: String(error) }),
+				data: JSON.stringify({ type: 'error', message: 'Event stream connection failed' }),
 			});
 		}
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 48ec784 and ab54402.

📒 Files selected for processing (2)
  • src/routes/tasks.ts
  • src/web/components/settings/ApiKeySettings.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/web/components/settings/ApiKeySettings.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
src/routes/tasks.ts (5)
src/db/index.ts (1)
  • db (11-11)
src/db/schema.ts (5)
  • workspaces (15-23)
  • chatSessions (25-40)
  • skills (42-53)
  • sources (55-64)
  • userSettings (66-72)
src/lib/encryption.ts (1)
  • decrypt (18-31)
src/lib/parse-metadata.ts (1)
  • parseMetadata (11-31)
src/routes/chat.ts (1)
  • handleSessionCompletionEvent (86-140)
🔇 Additional comments (7)
src/routes/tasks.ts (7)

15-33: LGTM!

The imports are well-organized, and the handleSessionCompletionEvent import from ./chat properly addresses the previous review feedback about webhook integration.


46-74: LGTM!

The race condition fix is correctly implemented using onConflictDoNothing() with a re-fetch fallback. This pattern safely handles concurrent workspace creation attempts.


79-89: LGTM!

Good security practice returning the same 404 error for both "not found" and "not owned" cases, preventing enumeration of other users' tasks.


196-274: LGTM!

The background task properly handles failures by setting the task status to 'error' with error metadata, addressing the previous review concern about tasks stuck in 'creating' state. The retry logic with 5 attempts and 2-second delays is reasonable.


289-310: LGTM!

Clean implementation that properly validates ownership and returns relevant task details.


422-426: LGTM!

Proper integration of handleSessionCompletionEvent for webhook delivery, addressing the previous review feedback. The fire-and-forget pattern with error logging is appropriate for non-blocking webhook delivery.


451-476: LGTM!

Robust cleanup implementation that:

  1. Handles sandbox destruction failures gracefully (logs and continues)
  2. Cleans up the OpenCode client
  3. Deletes the DB record last to avoid orphaned external resources

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant